﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Numerics;
using System.Text;

namespace AkzenteIT.Util.Asn1
{
    public class BerReader
    {
        private readonly Stack<InternalNode> nodeStack = new Stack<InternalNode>();
        private InternalNode current = new DocumentStartNode();
        private readonly Stream innerStream;

        public BerReader(Stream stream)
        {
            innerStream = stream;
        }

        public int Depth
        {
            get { return nodeStack.Count; }
        }

        public Asn1NodeType NodeType
        {
            get { return current.NodeType; }
        }

        public bool EndOfDocument
        {
            get { return current is DocumentEndNode; }
        }

        public bool HasIndefiniteLength
        {
            get { return Length == -1; }
        }

        /// <summary>
        ///     Length of the current element or -1 if indefinite
        /// </summary>
        public long Length
        {
            get { return current.Length; }
        }


        public Asn1Type Type
        {
            get { return current.Type; }
        }

        public bool IsConstructed
        {
            get { return current.IsConstructed; }
        }

        public void ReadStartElement()
        {
            if (NodeType != Asn1NodeType.ConstructedStart)
            {
                throw new InvalidOperationException("Content cannot be read at this position");
            }

            Read();
        }

        public void ReadEndElement()
        {
            if (NodeType != Asn1NodeType.ConstructedEnd)
            {
                throw new InvalidOperationException("Content cannot be read at this position");
            }

            Read();
        }

        public byte[] ReadContentAsBuffer()
        {
            if (NodeType != Asn1NodeType.Primitive)
            {
                throw new InvalidOperationException("Content cannot be read at this position");
            }

            if (IsConstructed || HasIndefiniteLength)
            {
                throw new InvalidOperationException("Cannot read value from constructed type");
            }

            if (Length > int.MaxValue)
            {
                throw new InvalidOperationException("Cannot read values larger than 2GB into buffer");
            }

            byte[] result = new byte[(int) Length];
            innerStream.Read(result, 0, (int)Length);

            Read();

            return result;
        }

        public object ReadContentAsValue()
        {
            switch (Type)
            {
                case Asn1Type.ObjectIdentifier:
                    return ReadContentAsObjectIdentifier();
                case Asn1Type.PrintableString:
                case Asn1Type.Ia5String:
                case Asn1Type.Utf8String:
                case Asn1Type.UtcTime: // TODO: Convert to DateTime
                    return ReadContentAsString();
                case Asn1Type.Integer:
                    return ReadContentAsBigInteger();
            }

            return ReadContentAsBuffer();
        }

        public BigInteger ReadContentAsBigInteger()
        {
            return new BigInteger(ReadContentAsBuffer());
        }

        private string ReadContentAsString()
        {
            return Encoding.UTF8.GetString(ReadContentAsBuffer());
        }

        public ObjectIdentifier ReadContentAsObjectIdentifier()
        {
            if (Type != Asn1Type.ObjectIdentifier)
            {
                throw new InvalidOperationException("Current value ");
            }
            return ObjectIdentifier.ParseEncoded(ReadContentAsBuffer());
        }


        public void Read()
        {
            if (current.NodeType == Asn1NodeType.None)
            {
                InternalReadElement();
            }
            else if (current.NodeType == Asn1NodeType.Primitive)
            {
                innerStream.Seek(current.EndPosition, SeekOrigin.Begin);
                InternalReadElement();
            }
            else if (current.NodeType == Asn1NodeType.ConstructedStart)
            {
                nodeStack.Push(current);
                InternalReadElement();
            }
            else if (current.NodeType == Asn1NodeType.ConstructedEnd)
            {
                nodeStack.Pop();
                InternalReadElement();
            }
        }

        private void InternalReadElement()
        {
            if (nodeStack.Count > 0 && innerStream.Position >= nodeStack.Peek().EndPosition)
            {
                current = new ConstructionEndNode
                          {
                              EndPosition = innerStream.Position,
                              Length = 0,
                              Type = nodeStack.Peek().Type
                          };

                return;
            }

            int tagOrEndOfStream = innerStream.ReadByte();

            if (tagOrEndOfStream < 0)
            {
                current = new DocumentEndNode();
                return;
            }

            byte tagStart = (byte)tagOrEndOfStream;

            //parse Tag
            bool isConstructed = (tagStart & 0x20) != 0;

            bool moreBytes = (tagStart & 0x1F) == 0x1F;

            while (moreBytes)
            {
                byte b = ReadByteOrThrow(innerStream);

                // TODO parse multi byte tag
                throw new NotSupportedException();

                moreBytes = (b & 0x80) != 0;
            }

            var type = (Asn1Type) (tagStart & ~32);

            //parse Length
            byte lengthStart = ReadByteOrThrow(innerStream);

            if (lengthStart == 0xff)
            {
                throw new FormatException("Invalid length format.");
            }

            int length = 0;
            if (lengthStart == 0x80)
            {
                // indefinite form
                length = -1;
                throw new NotSupportedException();
            }
            bool isMultiByteLength = (lengthStart & 0x80) != 0;

            if (!isMultiByteLength)
            {
                length = lengthStart;
            }
            else
            {
                int numBytesLength = lengthStart & 0x7f;

                if (numBytesLength > 4)
                {
                    throw new FormatException("Invalid length value (too long)");
                }

                while (numBytesLength > 0)
                {
                    length = (length << 8) | ReadByteOrThrow(innerStream);
                    numBytesLength--;
                }
            }

            if (isConstructed)
            {
                current = new ConstructionStartNode
                          {
                              EndPosition = innerStream.Position + length,
                              Length = length,
                              Type = type
                          };
            }
            else
            {
                current = new PrimitiveNode
                          {
                              EndPosition = innerStream.Position + length,
                              Length = length,
                              Type = type
                          };
            }
        }

        private byte ReadByteOrThrow(Stream stream)
        {
            int b = stream.ReadByte();

            if (b < 0)
            {
                throw new EndOfStreamException();
            }

            return (byte) b;
        }


        internal class ConstructionEndNode : InternalNode
        {
            public override bool IsConstructed
            {
                get { return true; }
            }

            public override Asn1NodeType NodeType
            {
                get { return Asn1NodeType.ConstructedEnd; }
            }
        }

        internal class ConstructionStartNode : InternalNode
        {
            public override bool IsConstructed
            {
                get { return true; }
            }

            public override Asn1NodeType NodeType
            {
                get { return Asn1NodeType.ConstructedStart; }
            }
        }

        internal class DocumentEndNode : InternalNode
        {
            public override bool IsConstructed
            {
                get { return true; }
            }

            public override Asn1NodeType NodeType
            {
                get { return Asn1NodeType.None; }
            }
        }

        internal class DocumentStartNode : InternalNode
        {
            public override bool IsConstructed
            {
                get { return true; }
            }

            public override Asn1NodeType NodeType
            {
                get { return Asn1NodeType.None; }
            }
        }

        internal abstract class InternalNode
        {
            public long EndPosition { get; set; }
            public int Length { get; set; }
            public Asn1Type Type { get; set; }
            public abstract bool IsConstructed { get; }

            public abstract Asn1NodeType NodeType { get; }
        }

        internal class PrimitiveNode : InternalNode
        {
            public override bool IsConstructed
            {
                get { return false; }
            }

            public override Asn1NodeType NodeType
            {
                get { return Asn1NodeType.Primitive; }
            }
        }
    }
}